package kele.lc;

import kele.lc.utils.TreeNode;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ForkJoinPool;

/**
 * @ClassName : dynamicProgramming
 * @Author : kele
 * @Date: 2024/8/15 21:10
 * @Description : 动态规划算法题
 */
public class dynamicProgramming {

    /**
     * 343. 整数拆分
     * <p>
     * 给定一个正整数 n，将其拆分为至少两个正整数的和，并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
     * <p>
     * 示例 1:
     * 输入: 2
     * 输出: 1
     * 解释: 2 = 1 + 1, 1 × 1 = 1。
     * <p>
     * 思路：
     * dp的定义：最大的乘积
     * 转移方程：①拆成两份：(i-j)*j  ②拆成多份：(i-j)*dp[j]
     *
     * @param n：输入的整数
     */
    public static void integerBreak(int n) {

        int[] dp = new int[n + 1];
        dp[1] = 1;

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                dp[i] = Math.max(dp[i], Math.max((i - j) * dp[j], j * (i - j)));
            }
        }
        System.out.println(dp[n]);
    }

    /**
     * 1049.最后一块石头的重量 II
     * 有一堆石头，用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
     * <p>
     * 每一回合，从中选出任意两块石头，然后将它们一起粉碎。假设石头的重量分别为 x 和 y，且 x <= y。那么粉碎的可能结果如下：
     * <p>
     * 如果 x == y，那么两块石头都会被完全粉碎；
     * 如果 x != y，那么重量为 x 的石头将会完全粉碎，而重量为 y 的石头新重量为 y-x。
     * 最后，最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下，就返回 0。
     *
     * @param stones：每块石头的重量： 思路：
     *                        将石头分成两份，计算两份重量的差就是最后会剩下的石头
     *                        转为01背包问题：
     *                        目标体积：sum/2
     *                        重量：每块石头的重量
     *                        价值：每块石头的重量
     */
    public static void lastStoneWeightII(int[] stones) {

        int sum = Arrays.stream(stones).sum();
        int target = sum / 2;

        int[] dp = new int[target + 1];

        for (int i = 0; i < stones.length; i++) {
            for (int j = target; j >= stones[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }

        System.out.println(Math.abs(sum - 2 * dp[target]));
    }

    /**
     * 494. 目标和
     * 给你一个非负整数数组 nums 和一个整数 target 。
     * 向数组中的每个整数前添加 '+' 或 '-' ，然后串联起所有整数，可以构造一个 表达式 ：
     * 例如，nums = [2, 1] ，可以在 2 之前添加 '+' ，在 1 之前添加 '-' ，然后串联起来得到表达式 "+2-1" 。
     * 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
     *
     * @param nums：提供的数组
     * @param target：需要构造的整数 思路：
     *                       假设+的数字的和为x, 则需要-的数字和为：x - (sum-x) = target => x = (target+sum)/2
     *                       转化为01背包问题：求能满足+的数字的和能到达(target+sum)/2的个数
     */
    public static void findTargetSumWays(int[] nums, int target) {

        int sum = Arrays.stream(nums).sum();
//        x - (sum-x) = target => x = (target+sum)/2
//        x必须是整数  所以需要满足以下条件
        if (Math.abs(target) > sum || (target + sum) % 2 != 0) {
            System.out.println(0);
            return;
        }

        int add_sum = (target + sum) / 2;
        int[] dp = new int[add_sum + 1];
        dp[0] = 1;

        for (int i = 0; i < nums.length; i++) {
            for (int j = add_sum; j >= nums[i]; j--) {
                dp[j] = dp[j] + dp[j - nums[i]];
                System.out.println(dp[j] + "\t");
            }
            System.out.println();
        }

        System.out.println(dp[add_sum]);

    }


    /**
     * 416. 分割等和子集
     * <p>
     * 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。
     * 注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
     * <p>
     * 示例 1:
     * 输入: [1, 5, 11, 5]
     * 输出: true
     * 解释: 数组可以分割成 [1, 5, 5] 和 [11].
     * <p>
     * 思路：
     * dp[tar]：在tar时，获取的最大的价值
     *
     * @param nums：输入的数组
     */
    public static void canPartition(int[] nums) {

        int sum = Arrays.stream(nums).sum();
        if (sum % 2 != 0) {
            System.out.println(false);
            return;
        }

        int tar = sum / 2;

        int[] dp = new int[tar + 1];

        for (int i = 0; i < nums.length; i++) {
            for (int j = tar; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        System.out.println(tar == dp[tar]);
    }


    /**
     * 322. 零钱兑换
     * 给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。
     * 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。
     * 你可以认为每种硬币的数量是无限的。
     * <p>
     * 思路：
     * dp[j]的含义：当总钱数为j时，需要最小的硬币数
     * 转移方程：dp[j] = Min( dp[j],dp[j-count[i]] + 1 )
     * <p>
     * 无限个：j从头开始
     * 不考虑顺序：先遍历物品，再遍历质量
     *
     * @param coins：零钱面值
     * @param amount：零钱总数
     */
    public static void coinChange(int[] coins, int amount) {

        int len = coins.length;

        int[] dp = new int[amount + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 0; i < len; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                if (dp[j - coins[i]] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        System.out.println(dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount]);
    }

    /**
     * 377.组合总和4
     * 给定一个由正整数组成且不存在重复数字的数组，找出和为给定目标正整数的组合的个数。
     * <p>
     * 示例:
     * nums = [1, 2, 3]
     * target = 4
     * 所有可能的组合为： (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
     * <p>
     * 请注意，顺序不同的序列被视作不同的组合。
     * 因此输出为 7。
     * <p>
     * 思路：
     * dp[j]：到j时有的组合数
     * 转移方程：dp[j] += dp[j-nums[i]]
     * 要考虑组合顺序
     *
     * @param nums：输入的数组
     * @param target：需要的目标
     */
    public static void combinationSum4(int[] nums, int target) {

        int len = nums.length;
        int[] dp = new int[target + 1];

        dp[0] = 1;

        for (int j = 0; j <= target; j++) {
            for (int i = 0; i < len; i++) {
                if (j >= nums[i]) {
                    dp[j] += dp[j - nums[i]];
                }
            }
        }
        System.out.println(dp[target]);
    }

    /**
     * 279.完全平方数
     * <p>
     * 给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, ...）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
     * 给你一个整数 n ，返回和为 n 的完全平方数的 最少数量 。
     * 完全平方数 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是。
     * <p>
     * 示例 1：
     * 输入：n = 12
     * 输出：3
     * 解释：12 = 4 + 4 + 4
     *
     * @param n：输入的总数
     */
    public static void numSquares(int n) {

        int[] dp = new int[n + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 0; i * i <= n; i++) {
            for (int j = i * i; j <= n; j++) {
                if (dp[j - i * i] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                }
            }
        }
        System.out.println(dp[n]);
    }

    /**
     * 518. 零钱兑换2
     * 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
     * <p>
     * 示例 1:
     * 输入: amount = 5, coins = [1, 2, 5]
     * 输出: 4
     * 5=5
     * 5=2+2+1
     * 5=2+1+1+1
     * 5=1+1+1+1+1
     *
     * @param amount：总金额
     * @param coins：硬币组合
     */
    public static void change(int amount, int[] coins) {

        int len = coins.length;

        int[] dp = new int[amount + 1];

        dp[0] = 1;
        for (int i = 0; i < len; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] += dp[j - coins[i]];
                System.out.print(dp[j] + "\t");
            }
            System.out.println();
        }
        System.out.println(dp[amount]);
    }

    /**
     * 474. 一和零
     * 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
     * 请你找出并返回 strs 的最大子集的长度，该子集中 最多 有 m 个 0 和 n 个 1 。
     * 如果 x 的所有元素也是 y 的元素，集合 x 是集合 y 的 子集
     *
     * @param strs：输入的字符串
     * @param m：0的个数
     * @param n：1的个数      思路：二维背包问题
     *                    每个str串可以选也可以不选，所以是背包问题
     *                    重量：每个串中0、1的个数
     *                    价值：每个串的价值都是1
     *                    体积：要求的m,n就是体积
     */
    public static void findMaxForm(String[] strs, int m, int n) {

        int[][] dp = new int[m + 1][n + 1];

        for (String str : strs) {
            int zero_sum = 0;
            int one_num = 0;

            for (char s : str.toCharArray()) {

                if (s == '0') {
                    zero_sum++;
                }
                if (s == '1') {
                    one_num++;
                }

            }

            for (int i = m; i >= zero_sum; i--) {
                for (int j = n; j >= one_num; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - zero_sum][j - one_num] + 1);
                }
            }
        }

        System.out.println(dp[m][n]);

    }

    /**
     * 139. 单词拆分
     * 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
     * <p>
     * 说明：
     * 拆分时可以重复使用字典中的单词。
     * 你可以假设字典中没有重复的单词。
     * <p>
     * 示例 1：
     * <p>
     * 输入: s = "leetcode", wordDict = ["leet", "code"]
     * 输出: true
     * 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
     *
     * @param s：字符串
     * @param wordDict：列表
     */
    public static void wordBreak(String s, List<String> wordDict) {

        HashSet<String> set = new HashSet<>(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;

        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (set.contains(s.substring(j, i)) && dp[j]) {
                    dp[i] = true;
                }
            }
        }
        System.out.println(dp[s.length()]);

    }

    /**
     * 198.打家劫舍
     * 你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，
     * 如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。
     * 给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。
     * <p>
     * 示例 1：
     * 输入：[1,2,3,1]
     * 输出：4
     * <p>
     * 要考虑长度为1的情况
     *
     * @param asset:每个位置的资产
     */
    public static void robbery1(int[] asset) {

        if (asset == null || asset.length < 1) {
            System.out.println(0);
            return;
        }

        if (asset.length == 1) {
            System.out.println(asset[0]);
            return;
        }

        int[] dp = new int[asset.length];
        dp[0] = asset[0];

        dp[1] = Math.max(asset[0], asset[1]);  //需要考虑长度为1的情况，导致数组越界

        for (int i = 2; i < asset.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + asset[i]);
        }

        System.out.println(dp[asset.length - 1]);

    }

    /**
     * 213.打家劫舍二
     * 你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ，
     * 这意味着第一个房屋和最后一个房屋是紧挨着的。同时，相邻的房屋装有相互连通的防盗系统，
     * 如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警 。
     * 给定一个代表每个房屋存放金额的非负整数数组，计算你 在不触动警报装置的情况下 ，能够偷窃到的最高金额。
     * <p>
     * 输入：nums = [2,3,2]
     * 输出：3
     * 解释：你不能先偷窃 1 号房屋（金额 = 2），然后偷窃 3 号房屋（金额 = 2）, 因为他们是相邻的。
     * <p>
     * 思路：
     * 1. 需要考虑到首尾的情况
     * 转化为：①不考虑第一位，②不考虑末尾的
     * 对这两中情况求最大值
     *
     * @param asset:每个位置的资产
     */
    public static void robbery2(int[] asset) {

        int max = Math.max(rob(asset, 0, asset.length - 1), rob(asset, 1, asset.length));
        System.out.println(max);

    }

    public static int rob(int[] asset, int start, int end) {

        int x = 0, y = 0, z = 0;
        for (int i = start; i < end; i++) {
            z = Math.max(y, x + asset[i]);
            x = y;
            y = z;
            System.out.print(z + "\t");
        }
        System.out.println();
        return z;
    }


    /**
     * 在上次打劫完一条街道之后和一圈房屋后，小偷又发现了一个新的可行窃的地区。
     * 这个地区只有一个入口，我们称之为“根”。 除了“根”之外，每栋房子有且只有一个“父“房子与之相连。
     * 一番侦察之后，聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。
     * 如果两个直接相连的房子在同一天晚上被打劫，房屋将自动报警。
     * <p>
     * 思路：
     * 考虑状态，每个节点有两种状态，0：不包含本节点的最大值  1：包含本节点的最大值
     * 每个节点的0[不包含本节点值]状态 = 左节点0、1状态的最大值 + 右节点0、1状态的最大值
     * 每个节点的1[包含本节点值]状态 = 当前节点的值 + 左节点0的状态 + 右节点0的状态
     *
     * @param root
     */
    public static void robbery3(TreeNode root) {
        int[] res = rob3(root);
        System.out.println(Math.max(res[0], res[1]));

    }

    public static int[] rob3(TreeNode root) {
        int[] res = new int[2];

        if (root == null) {
            return res;
        }

        int[] left = rob3(root.left);
        int[] right = rob3(root.right);

        res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        res[1] = root.val + left[0] + right[0];

        return res;
    }

    /**
     * 121.买卖股票问题1
     * <p>
     * 给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
     * 你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
     * 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。
     * <p>
     * 示例 1：
     * 输入：[7,1,5,3,6,4]
     * 输出：5
     * 解释：在第 2 天（股票价格 = 1）的时候买入，在第 5 天（股票价格 = 6）的时候卖出，
     * 最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。
     * <p>
     * 总结：
     * 1、
     *
     * @param prices：每天的股票价格
     */
    public static void maxProfit(int[] prices) {

        // 记录一次交易，一次交易有买入卖出两种状态
        // 0代表买入，1代表卖出
        int[] dp = new int[2];

        dp[0] = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], -prices[i]);  //状态0 [买入状态] = 之前就买入，今天买入
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);  //状态1 [卖出状态] = 之前就卖出，之前买入今天卖出
        }
        System.out.println(dp[1]);

    }


    /**
     * 122. 买卖股票的最佳时机
     * 给定一个数组，它的第 i 个元素是一支给定股票第 i 天的价格。
     * 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易（多次买卖一支股票）。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     * 示例 1:
     * 输入: [7,1,5,3,6,4]
     * 输出: 7
     * 解释: 在第 2 天（股票价格 = 1）的时候买入，在第 3 天（股票价格 = 5）的时候卖出,
     * 这笔交易所能获得利润 = 5-1 = 4。随后，在第 4 天（股票价格 = 3）的时候买入，
     * 在第 5 天（股票价格 = 6）的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
     * <p>
     * 总结：
     * 1、
     *
     * @param prices:每天股票价格
     */
    public static void maxProfit2(int[] prices) {

        // 记录一次交易，一次交易有买入卖出两种状态
        // 0代表买入，1代表卖出
        int[] dp = new int[2];

        dp[0] = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], dp[1] - prices[i]);  //状态0 [买入状态] = 之前就买入，之前卖出+今天买入
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);  //状态1 [卖出状态] = 之前就卖出，之前买入今天卖出
        }
        System.out.println(dp[1]);

    }

    /**
     * 123.买卖股票的最佳时机 问题三 【最多可以买卖两次】
     * 给定一个数组，它的第 i 个元素是一支给定的股票在第 i 天的价格。
     * 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     * <p>
     * 示例 1:
     * 输入：prices = [3,3,5,0,0,3,1,4]
     * 输出：6 解释：在第 4 天（股票价格 = 0）的时候买入，在第 6 天（股票价格 = 3）的时候卖出，
     * 这笔交易所能获得利润 = 3-0 = 3 。随后，在第 7 天（股票价格 = 1）的时候买入，
     * 在第 8 天 （股票价格 = 4）的时候卖出，这笔交易所能获得利润 = 4-1 = 3。
     * <p>
     * 思路：
     * 一共四种状态：
     * 0：第一次买入
     * 1：第一次卖出
     * 2：第二次买入
     * 3：第二次卖出
     *
     * @param prices：当天的股票价格
     */
    public static void maxProfit3(int[] prices) {

        // 记录一次交易，一次交易有买入卖出两种状态
        // 0代表买入，1代表卖出
        int[] dp = new int[4];

        dp[0] = -prices[0];  //第一次买入，所以要扣钱
        dp[2] = -prices[0];  //第二次买入，为什么是-price[0]？ 第一天第一次买卖了一次，第二次再买入

        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], -prices[i]);         //状态0 [第一次买入状态] = 之前买入，当天买入
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);  //状态1 [第一次卖出状态] = 第一次之前已经卖出了，之前买入今天卖出
            dp[2] = Math.max(dp[2], dp[1] - prices[i]);  //状态2 [第二次买入状态] = 第二次已经买入了，第一次卖出了今天买入算第二次买入
            dp[3] = Math.max(dp[3], dp[2] + prices[i]);  //状态3 [第二次卖出状态] = 第二次已经卖出了，第二次之前买入了，今天卖出
        }
        System.out.println(dp[3]);

    }

    /**
     * 188. 买卖股票的最佳时机
     * 给定一个整数数组 prices ，它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
     * 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     * 示例 1：
     * 输入：k = 2, prices = [2,4,1]
     * 输出：2 解释：在第 1 天 (股票价格 = 2) 的时候买入，在第 2 天 (股票价格 = 4) 的时候卖出，这笔交易所能获得利润 = 4-2 = 2。
     * <p>
     * 总结：
     * 1、因为可以买卖k次，每次有买入、卖出两种状态，所以一共有2*k状态
     *
     * @param prices：当天的价格
     * @param k：买卖的次数
     */
    public static void maxProfit4(int[] prices, int k) {

        // 记录一次交易，一次交易有买入卖出两种状态
        // 0，偶数代表买入，奇数代表卖出
        int[] dp = new int[2 * k];

        for (int i = 0; i < k; i++) {
            dp[i * 2] = -prices[0];
        }

        for (int i = 1; i < prices.length; i++) {

            for (int j = 0; j < 2 * k; j++) {
                if (j % 2 == 0) {
                    dp[j] = j == 0 ? Math.max(dp[j], -prices[i]) : Math.max(dp[j], dp[j - 1] - prices[i]);
                } else {
                    dp[j] = Math.max(dp[j], dp[j - 1] + prices[i]);
                }
            }
        }
        System.out.println(dp[2 * k - 1]);

    }


    /**
     * 给定一个整数数组，其中第 i 个元素代表了第 i 天的股票价格 。
     * 设计一个算法计算出最大利润。在满足以下约束条件下，你可以尽可能地完成更多的交易（多次买卖一支股票）:
     * 你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     * 卖出股票后，你无法在第二天买入股票 (即冷冻期为 1 天)。
     * <p>
     * 示例:
     * 输入: [1,2,3,0,2]
     * 输出: 3
     * 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
     * <p>
     * 总结：
     * 本身应该3种状态，①买入  ②卖出  ③冷冻
     * 其中②卖出需要包含两种情况，之前卖出就度过了一天的冷却期，今天卖出、
     * 所以一共四种状态：①买入  ②之前卖出  ③今天卖出  ④冷却期
     *
     * @param prices:每天的价格
     */
    public static void maxProfit5(int[] prices) {

        int[] dp = new int[4];

        dp[0] = -prices[0];

        for (int i = 1; i < prices.length; i++) {

            int tmp = dp[0];
            int tmp2 = dp[2];
            dp[0] = Math.max(dp[0], Math.max(dp[1], dp[3]) - prices[i]);   //买入状态： 之前就买入了，之前卖出后又买入，过冷却期后又买入了
            dp[1] = Math.max(dp[1], dp[3]);               //之前卖出状态： 之前就卖出了，卖出后过了冷却期
            dp[2] = tmp + prices[i];                      //今天卖出状态： 之前买入+今天卖出
            dp[3] = tmp2;                                 //冷却期： 今天买入
        }
        System.out.println(Math.max(dp[1], Math.max(dp[2], dp[3])));
    }


    /**
     * 714. 买卖股票问题，包含手续费
     * 给定一个整数数组 prices，其中第 i 个元素代表了第 i 天的股票价格 ；非负整数 fee 代表了交易股票的手续费用。
     * 你可以无限次地完成交易，但是你每笔交易都需要付手续费。如果你已经购买了一个股票，在卖出它之前你就不能再继续购买股票了。
     * 返回获得利润的最大值。
     * 注意：这里的一笔交易指买入持有并卖出股票的整个过程，每笔交易你只需要为支付一次手续费。
     * <p>
     * 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
     * 输出: 8
     * <p>
     * 总结：
     * 0：买入状态  1：卖出状态
     *
     * @param prices:每次的价格
     * @param fee:买卖时需要的手续费
     */
    public static void maxProfit6(int[] prices, int fee) {

        int[] dp = new int[2];

        dp[0] = -prices[0] - fee;  //初始化，第一次购买不仅需要支付购买费用，还有手续费

        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], dp[1] - prices[i] - fee);  //0[买入状态]：之前就买入了，卖出后买入，扣除手续费
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);        //1[卖出状态]：之前就卖出了，买入后卖出
        }
        System.out.println(dp[1]);
    }


    /**
     * 300. 最长递增子序列
     *
     * @param nums：输入的序列
     */
    public static void lengthOfLIS(int[] nums) {

        int len = nums.length;
        int[] dp = new int[len];
        Arrays.fill(dp, 1);

        int max = 0;
        for (int i = 1; i < len; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
                max = Math.max(max, dp[i]);
                System.out.print(dp[i] + "\t");
            }
            System.out.println();
        }
        System.out.println(max);

    }

    /**
     * 674. 最长连续递增子序列
     * 给定一个未经排序的整数数组，找到最长且 连续递增的子序列，并返回该序列的长度
     * 连续递增的子序列 可以由两个下标 l 和 r（l < r）确定，如果对于每个 l <= i < r，都有 nums[i] < nums[i + 1] ，
     * 那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列
     * <p>
     * 示例
     * 输入：nums = [1,3,5,4,7]
     * 输出：3
     * 解释：最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的，因为 5 和 7 在原数组里被 4 隔开。
     *
     * @param nums
     */
    public static void findLengthOfLCIS(int[] nums) {

        if (nums.length <= 1) {
            System.out.println(nums.length);
            return;
        }

        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int max = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                dp[i] = dp[i - 1] + 1;
            }
            max = Math.max(max, dp[i]);
        }
        System.out.println(max);
    }


    /**
     * 718. 最长重复子数组
     * 给两个整数数组 A 和 B ，返回两个数组中公共的、长度最长的子数组的长度。
     * 示例：
     * 输入：
     * A: [1,2,3,2,1]
     * B: [3,2,1,4,7]
     * 输出：3
     * 解释：长度最长的公共子数组是 [3, 2, 1] 。
     *
     * @param nums1:数组1
     * @param nums2:数组2
     */
    public static void findLength(int[] nums1, int[] nums2) {

        int len1 = nums1.length;
        int len2 = nums2.length;

        int[][] dp = new int[len1][len2];

        int max = 0;
        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                if (nums1[i] == nums2[j]) {
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1;
                    } else {
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                    }
                }
                max = Math.max(max, dp[i][j]);
            }
        }
        System.out.println(max);
    }

    /**
     * 1143.最长公共子序列
     * 给定两个字符串 text1 和 text2，返回这两个字符串的最长公共子序列的长度。
     * 一个字符串的 子序列 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。
     * 例如，"ace" 是 "abcde" 的子序列，但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
     * 若这两个字符串没有公共子序列，则返回 0。
     * <p>
     * 示例 1:
     * 输入：text1 = "abcde", text2 = "ace"
     * 输出：3
     * 解释：最长公共子序列是 "ace"，它的长度为 3。
     *
     * @param text1:输入字符串1
     * @param text2：输入字符串2
     */
    public static void longestCommonSubsequence(String text1, String text2) {

//        int len1 = text1.length();
//        int len2 = text2.length();
//        System.out.println(len1 + "  " + len2);
//
//        int[][] dp = new int[len1][len2];
//
//        for (int i = 0; i < len1; i++) {
//            for (int j = 0; j < len2; j++) {
//                if (i == 0 || j == 0) {
//                    if (text1.charAt(i) == text2.charAt(j)) {
//                        dp[i][j] = 1;
//                    } else {
//                        if (i == 0 && j == 0) {
//                            dp[i][j] = 0;
//                        } else if (j == 0) {
//                            dp[i][j] = dp[i - 1][j];
//                        } else {
//                            dp[i][j] = dp[i][j - 1];
//                        }
//                    }
//
//                } else {
//                    if (text1.charAt(i) == text2.charAt(j)) {
//                        dp[i][j] = Math.max(dp[i - 1][j], Math.max(dp[i][j - 1], dp[i - 1][j - 1]+1));
//                    }else{
//                        dp[i][j] = Math.max(dp[i - 1][j], Math.max(dp[i][j - 1], dp[i - 1][j - 1]));
//                    }
//                }
//                System.out.print(dp[i][j] + "\t");
//            }
//            System.out.println();
//        }
//        System.out.println(dp[len1 - 1][len2 - 1]);
//

        /*************  优化后   *********************/
        /**
         * 优化点：1、如果dp[i] 从0开始，需要考虑每个i=0，j=0的情况，修改为从1开始，到len结束
         */
        int len1 = text1.length();
        int len2 = text2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 1; i <= len1; i++) {
            char t1 = text1.charAt(i - 1);
            for (int j = 1; j <= len2; j++) {
                char t2 = text2.charAt(j - 1);
                if (t1 == t2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        System.out.println(dp[len1][len2]);
    }

    /**
     * 1035. 不相交的线
     * 我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
     * 现在，我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线，只要 A[i] == B[j]，且我们绘制的直线不与任何其他连线（非水平线）相交。
     * 以这种方法绘制线条，并返回我们可以绘制的最大连线数。
     * <p>
     * 总结：
     * 五部曲
     * 1、dp的含义：dp[i][j] 代表以i结尾的A和以j结尾的B，他们之间最多几条不相交的线
     * 2、递推公式：dp
     *
     * @param nums1：输入数组1
     * @param nums2：输入数组2
     */
    public static void maxUncrossedLines(int[] nums1, int[] nums2) {

        int len1 = nums1.length;
        int len2 = nums2.length;

        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    /**
                     *
                     */
                    dp[i][j] = dp[i - 1][j - 1] + 1;   //这里i  j  和nums中的i,j相差1，注意他的意义
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        System.out.println(dp[len1][len2]);
    }


    /**
     * 53. 最大子数和
     * <p>
     * 给定一个整数数组 nums ，找到一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。
     * <p>
     * 示例:
     * 输入: [-2,1,-3,4,-1,2,1,-5,4]
     * 输出: 6
     * 解释: 连续子数组 [4,-1,2,1] 的和最大，为 6。
     * <p>
     * 思路：
     * dp[i]含义: 从0到当前位置最大的值
     * 转移方程：当前位置的最大值 = 要么从之前的位置开始累加计算，要么从当前位置开始累加计算
     *
     * @param nums：输入的字符串
     */
    public static void maxSubArray(int[] nums) {

        int len = nums.length;

        int[] dp = new int[len];
        dp[0] = nums[0];
        int max = 0;

        for (int i = 1; i < len; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);  //当前位置的最大值 = 从之前的位置开始累加，从当前位置开始累加
            max = Math.max(max, dp[i]);
        }
        System.out.println(max);
    }

    /**
     * 392. 判断子序列
     * <p>
     * 给定字符串 s 和 t ，判断 s 是否为 t 的子序列。
     * 字符串的一个子序列是原始字符串删除一些（也可以不删除）字符而不改变剩余字符相对位置形成的新字符串。
     * （例如，"ace"是"abcde"的一个子序列，而"aec"不是）。
     * <p>
     * 示例 1：
     * 输入：s = "abc", t = "ahbgdc"
     * 输出：true
     *
     * @param s:输入字符串
     * @param t：输入字符串
     */
    public static void isSubsequence(String s, String t) {

        int len1 = s.length();
        int len2 = t.length();

        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; i++) {
            char cs = s.charAt(i - 1);
            for (int j = 1; j <= len2; j++) {
                char ct = t.charAt(j - 1);
                if (cs == ct) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        System.out.println(len1 == dp[len1][len2]);
    }

    /**
     * 115. 不同的子序列
     * 给定一个字符串 s 和一个字符串 t ，计算在 s 的子序列中 t 出现的个数。
     * 字符串的一个 子序列 是指，通过删除一些（也可以不删除）字符且不干扰剩余字符相对位置所组成的新字符串。
     * （例如，"ACE" 是 "ABCDE" 的一个子序列，而 "AEC" 不是）
     * 题目数据保证答案符合 32 位带符号整数范围。
     * <p>
     * 思路：
     * 1.dp[i][j]: 以j结尾的子串t在以i结尾的s串中出现的个数
     * 2.转移方程：如果si == tj, 则dp[i][j] = 之前相等的结果dp[i-1][j-1] + 用s之前的结果匹配，重复的字符串
     * 如果si != tj, 则dp[i][j] = 之前的结果dp[i-1][j]  //一定要和j长度的数据匹配上
     * 3.初始化：如果s为空，要匹配j结尾的t，则为0，如果j为空，要s匹配j结尾的t，则应该为1，如如果s，t都为空，则设置为1
     *
     * @param s:原始字符串
     * @param t:子串
     */
    public static void numDistinct(String s, String t) {

        int lens = s.length();
        int lent = t.length();

        int[][] dp = new int[lens + 1][lent + 1];
        for (int i = 0; i < lens; i++) {
            dp[i][0] = 1;
        }

        for (int i = 1; i <= lens; i++) {
            for (int j = 1; j <= lent; j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j]);
                }
            }
        }

        for (int i = 0; i <= lens; i++) {
            if (i == 0) {
                System.out.print(" " + "\t");
            } else {
                System.out.print(s.charAt(i - 1) + "\t");
            }
            for (int j = 0; j <= lent; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }
        System.out.println(dp[lens][lent]);
    }

    /**
     * 583. 两个字符串的删除操作
     * <p>
     * 给定两个单词 word1 和 word2，找到使得 word1 和 word2 相同所需的最小步数，每步可以删除任意一个字符串中的一个字符。
     * <p>
     * 示例：
     * 输入: "sea", "eat"
     * 输出: 2
     * 解释: 第一步将"sea"变为"ea"，第二步将"eat"变为"ea"
     * 思路：
     * 1. dp含义：需要删除的个数
     * 2.转移方程：①如果si==tj 那么 dp[i][j] = dp[i-1][j-1]  //不删除
     * ②如果si != sj 那么 dp[i-1][j-1]+2 or dp[i-1][j]+1 or dp[i][j-1]+1;  //删除2个 or 删除1个
     *
     * @param word1:单词1
     * @param word2：单词2
     */
    public static void minDistance(String word1, String word2) {

        int len1 = word1.length();
        int len2 = word2.length();

        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
        }

        for (int j = 0; j <= len2; j++) {
            dp[0][j] = j;
        }

        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j] + 1, Math.min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2));
                }
            }
        }

        for (int i = 0; i <= len1; i++) {
            if (i == 0) {
                System.out.print(" " + "\t");
            } else {
                System.out.print(word1.charAt(i - 1) + "\t");
            }
            for (int j = 0; j <= len2; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }

        System.out.println(dp[len1][len2]);
    }

    /**
     * 72. 编辑距离
     * 给你两个单词 word1 和 word2，请你计算出将 word1 转换成 word2 所使用的最少操作数 。
     * 你可以对一个单词进行如下三种操作：
     * 插入一个字符
     * 删除一个字符
     * 替换一个字符
     * <p>
     * 示例 1：
     * 输入：word1 = "horse", word2 = "ros"
     * 输出：3
     * 解释： horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
     *
     * @param word1
     * @param word2
     */
    public static void minDistance2(String word1, String word2) {

        int len1 = word1.length();
        int len2 = word2.length();

        int[][] dp = new int[len1 + 1][len2 + 1];

        for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
        }

        for (int j = 0; j <= len2; j++) {
            dp[0][j] = j;
        }

        for (int i = 1; i <= len1; i++) {
            char w1 = word1.charAt(i - 1);
            for (int j = 1; j <= len2; j++) {
                char w2 = word2.charAt(j - 1);
                if (w1 == w2) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
                }
            }
        }
        System.out.println(dp[len1][len2]);
    }

    /**
     * 647. 回文子串
     * <p>
     * 给定一个字符串，你的任务是计算这个字符串中有多少个回文子串。
     * 具有不同开始位置或结束位置的子串，即使是由相同的字符组成，也会被视作不同的子串。
     * <p>
     * 示例 1：
     * 输入："abc"
     * 输出：3
     * 解释：三个回文子串: "a", "b", "c"
     *
     * @param s：字符串
     */
    public static void countSubstrings(String s) {

        int len = s.length();
        boolean[][] dp = new boolean[len][len];
        int max = 0;

        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                if (s.charAt(i) == s.charAt(j) && (j - i < 3 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    max++;
                }
            }
        }
        System.out.println(max);
    }

    /**
     * 516.最长回文子序列
     * <p>
     * 给你一个字符串 s ，找出其中最长的回文子序列，并返回该序列的长度。
     * 子序列定义为：不改变剩余字符顺序的情况下，删除某些字符或者不删除任何字符形成的一个序列。
     *
     * @param s：字符串
     */
    public static void longestPalindromeSubseq(String s) {

        int len = s.length();

        int[][] dp = new int[len][len];

        for (int i = 1; i < len; i++) {
            dp[i][i] = 1;
            for (int j = i - 1; j >= 0; j--) {
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i - 1][j + 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j + 1]);
                }
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }

        System.out.println(dp[len - 1][0]);

    }

    public static void main(String[] args) {

        /**********************   子序列问题  **********************/
//        longestPalindromeSubseq("cbbd");
//        countSubstrings("aaa");    //回文子串   判断回文串的个数
//        minDistance2("intention", "execution");  //编辑距离   增删改操作，两个字符串相等的最小操作
//        minDistance("leetcode", "etco");   //两个字符串的删除操作， 判断t和s相等需要删除多少次
//        numDistinct("rabbbit", "rabbit");  //不同的子序列, 判断t在s中出现多少次，
//        isSubsequence("axc","ahbgdc");   //判断子序列  s是否是t的子串
//        maxSubArray(new int[]{-2, 1, -3, 4, -1, 2, 1, -5, 4}); //最大子序和  连续最大的子序列和最大
//        maxUncrossedLines(new int[]{2, 5, 1, 2, 5}, new int[]{10, 5, 2, 1, 5, 2}); //不相交的线  类似与求最长的公共子序列
//        longestCommonSubsequence("bsbininm", "jmjkbkjkv");  //最长公共子序列
//        findLength(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7});   //最长重复子数组
//        findLengthOfLCIS(new int[]{1,3,5,4,7});  //最长连续递增子序列：要求连续
//        lengthOfLIS(new int[]{1,3,6,7,9,4,10,5,6});  //最长递增子序列：不需要连续


        /******** 买卖股票问题  ********/
//        maxProfit6(new int[]{1, 3, 2, 8, 4, 9}, 2);  //买卖股票问题6：多次买卖，但是需要手续费
//        maxProfit5(new int[]{1, 2, 3, 0, 2});  //买卖股票问题5：多次买卖，但是包含冷冻期
//        maxProfit4(new int[]{2, 4, 1}, 2);  //买卖股票问题4：只能购买k次
//        maxProfit3(new int[]{1,2,3,4,5});  //买卖股票问题3：只能买卖2次
//        maxProfit2(new int[]{7, 1, 5, 3, 6, 4});  //买卖股票问题2：可以多次购买
//        maxProfit(new int[]{7,1,5,3,6,4});  //买卖股票问题1：只能购买一次

        /*****************  打家劫舍问题  *********************/
//        robbery3(new TreeNode(1));    //打家劫舍三，房屋二叉树方式排列
//        robbery2(new int[]{2, 3, 2});   //打家劫舍二，房屋会首尾相接
//        robbery1(new int[]{0});   //打家劫舍一

        /*********************  *****************************/
        wordBreak("leetcode", Arrays.asList("leet", "cod"));
        /*****************  背包问题   **************************/
//        numSquares(12);  //完全平方数  求n的平方数最小数
//        change(5, new int[]{1, 2, 5});  //零钱兑换2  求到目标数右多少种硬币方案
//        coinChange(new int[]{1, 2, 5}, 11);  //零钱兑换， 求需要【最少】的硬币数
//        combinationSum4(new int[]{1, 2, 3}, 4);   //组合总和4 达到target中需要nums的种类
//        canPartition(new int[]{1, 5, 11, 5});   //分割等和子集, 背包问题，不需要排序，不能重复、
//        findMaxForm(new String[]{"10", "0001", "111001", "1", "0"}, 5, 3);  //一和零
//        lastStoneWeightII(new int[]{2, 7, 4, 1, 8, 1});  //最后一块石头的重量  石头分成两部分，求两份的最小差
//        findTargetSumWays(new int[]{1, 1, 1, 1, 1}, 3);  //目标和


        /*****************  基础动态规划问题   **************************/
//        integerBreak(10);   //整数拆分
    }

}
